package com.yixing.tech.wms.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.yixing.tech.common.base.BusinessException;
import com.yixing.tech.common.convert.ConvertUtils;
import com.yixing.tech.common.CommonConstant;
import com.yixing.tech.common.entity.*;
import com.yixing.tech.common.service.AbstractWmsService;
import com.yixing.tech.wms.entity.InStockWork;
import com.yixing.tech.wms.entity.MixedMaterialGroup;
import com.yixing.tech.wms.param.ContainerBindMaterialParam;
import com.yixing.tech.wms.param.ToApplyParam;
import com.yixing.tech.wms.service.ErpReportService;
import com.yixing.tech.wms.service.StockLocationService;
import com.yixing.tech.wms.service.StockService;
import com.yixing.tech.wms.service.WmsTaskService;
import com.yixing.tech.wms.stock.out.domain.Task;
import com.yixing.tech.wms.stock.out.domain.vo.TaskType;
import com.yixing.tech.wms.utils.DocumentUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static com.yixing.tech.common.CommonConstant.*;
import static com.yixing.tech.common.CommonConstant.ContainerType.*;
import static com.yixing.tech.common.CommonConstant.InStatus.*;

/**
 * 入库服务
 *
 * @author yixing tech
 * @since 1.0.0
 */
@Slf4j
@Service
public abstract class AbstractInStockService extends AbstractWmsService {

    @Autowired
    private StockService stockService;
    @Autowired
    private StockLocationService stockLocationService;
    @Autowired
    private WmsTaskService wmsTaskService;
    @Autowired
    private StorageAreaServiceImpl storageAreaService;
    @Autowired
    private ErpReportService erpReportService;

    /**
     * 根据容器编码申请库位，申请当前空余的空位
     * 优先级排序：1. 无库存组。 有库存组：深度越大越优先
     * 更新[20241026]：A类库中小于等于4个将报错库位不足
     */
    @Transactional(rollbackFor = Exception.class)
    public Document doApplyLocationByContainerCode(String containerCode) throws BusinessException {
        if (StringUtils.isBlank(containerCode)) {
            throw new BusinessException("容器吗不能为空");
        }

        List<InStockWork> inStockTasks = mongoTemplate.find(Query.query(
                        newCriteria().and("containerCode").is(containerCode)
                                .and("inStatus").is(newDocument("id", new ObjectId(containerBind.getCode())))),
                InStockWork.class, "entity_" + IN_STOCK_TASK);
        if (CollectionUtils.isEmpty(inStockTasks)) {
            throw new BusinessException("上架作业不存在");
        }

        String materialCode = inStockTasks.get(0).getMaterialCode();
        // 存储规则
        Document rule = simpleFindOne("entity_" + STOCK_RULE, "materialCode", materialCode);
        if (rule == null) {
            throw new BusinessException("物料规则不存在");
        }

        Document material = simpleFindOne("entity_" + MATERIAL, "code", materialCode);
        String abc = material.getString("abc");

        List<String> areas = rule.getList("areas", String.class);
        if (CollectionUtils.isEmpty(areas)) {
            throw new BusinessException("物料没有对应的库区信息");
        }

        List<Document> locations = stockLocationService.queryEmptyLocation(areas, false);
        if (CollectionUtils.isEmpty(locations)) {
            throw new BusinessException("库区中无空闲库位");
        }
        assertBiz(() -> A.equals(abc) && locations.size() <= 4, "A库库位不足以分配");

        Document location = locations.get(0);

        // 上锁
        Update update = new Update();
        update.set("lock", true);
        if (!versionUpdateById(location, update, "entity_" + STOCK_LOCATION)) {
            throw new BusinessException("申请库位资源上锁失败！");
        }

        // 更新上架作业：入库状态改为待入库
        for (InStockWork inStockTask : inStockTasks) {
            Update update2 = new Update();
            update2.set("inStatus", newDocument("id", new ObjectId(inStocking.getCode())));
            update2.set("warehouseCode", Optional.ofNullable(location.get("areaObj", Document.class)).map(x -> x.getString("warehouse")).orElse(""));
            update2.set("areaCode", location.getString("area"));
            update2.set("locationCode", location.getString("code"));
            if (!updateById(inStockTask.get_id().toString(), update2, "entity_" + IN_STOCK_TASK)) {
                throw new BusinessException("更新上架作业失败！");
            }
        }
        return location;
    }

    /**
     * 根据多个原材料入库单申请容器
     */
    @Transactional(rollbackFor = Exception.class)
    public List<InStockWork> doApplyContainerByInOrders(List<String> inOrders, String orderTable, String orderDtlTable, String abc) throws BusinessException {
        if (CollectionUtils.isEmpty(inOrders)) {
            return Collections.emptyList();
        }
        List<InStockWork> resultList = applyContainerByInOrdersV2(inOrders, orderTable, orderDtlTable, abc);
        if (resultList.isEmpty()) {
            throw new BusinessException("没有可以申请容器的入库单明细");
        }

        // 创建容器下架任务
        if (!resultList.isEmpty()) {
            // 按照容器进行分组
            Map<String, List<InStockWork>> map = new LinkedHashMap<>();
            resultList.forEach(x -> map.computeIfAbsent(x.getContainerCode(), k -> new ArrayList<>()).add(x));
            for (String containerCode : map.keySet()) {
                List<InStockWork> list = map.get(containerCode);
                List<String> taskCodes = new LinkedList<>();
                list.forEach(x -> taskCodes.add(x.getCode()));
                InStockWork document = list.get(0);
                String locCode = document.getOutLocationCode();

                String targetAreaCode;
                if (A.equals(abc)) {
                    // 立体库
                    targetAreaCode = storageAreaService.validateGetFirstAreaCode(AreaType.stereoscopicSorting);
                } else {
                    // ctu
                    // targetAreaCode = storageAreaService.validateGetFirstAreaCode(AreaType.ctuSorting);
                    // B类物料CTU任务下架任务需要固定指定终点库区为  B类原材料拣货入口库区
                    targetAreaCode = AreaCodeDict.bMatPickingEntrance.getText();
                }
                Task task = Task.builder()
                        .taskCode(UUID.randomUUID().toString().replaceAll("-", ""))
                        .taskType(TaskType.STOCK_OUT.getId())
                        .startLoc(locCode)
                        .endArea(targetAreaCode)
                        .podCode(containerCode)
                        .business(IN_STOCK_BIZ)
                        .operationNo(taskCodes)
                        .build();
                wmsTaskService.saveTaskInfo(task);
            }
        }

//        // todo for test 模拟容器移动到拣货区 -----------------------------------
//        List<String> stockLocationCodes = resultList.stream().map(x -> x.getString("outLocationCode")).collect(Collectors.toList());
//        List<Document> updateStockLocations = mongoTemplate.find(Query.query(newCriteria().and("code").in(stockLocationCodes)),
//                Document.class, "entity_" + STOCK_LOCATION);
//        for (Document updateLocation : updateStockLocations) {
//            Update update = new Update();
//            update.set("lock", false);
//            update.unset("containerCode");
//            updateById(updateLocation, update, "entity_" + STOCK_LOCATION);
//        }
//        String materialLevel = Optional.ofNullable(resultList.get(0).get("level", Document.class))
//                .map(x -> x.get("id")).map(Object::toString).orElse(null);
//        List<String> locationCodes;
//        if(MaterialLevel.A.getCode().equals(materialLevel)) {
//            locationCodes = Arrays.asList("100102005", "100102004", "100102003", "100102002", "100102001");
//        } else if(MaterialLevel.B.getCode().equals(materialLevel)) {
//            locationCodes = Arrays.asList("100202001", "100202002", "100202003");
//        } else {
//            throw new BusinessException("未知的AB分类");
//        }
//        List<Document> locations = mongoTemplate.find(Query.query(newCriteria().and("code").in(locationCodes)),
//                Document.class, "entity_" + STOCK_LOCATION);
//        int i = 0, j = 0;
//        for (; i < locations.size() && j < resultList.size(); i++) {
//            Document location = locations.get(i);
//            String c = location.getString("containerCode");
//            boolean lock = location.getBoolean("lock");
//            if(lock || StringUtils.isNoneBlank(c)) {
//                continue;
//            }
//
//            String containerCode = resultList.get(j++).getString("containerCode");
//
//            Update update = new Update();
//            update.set("containerCode", containerCode);
//            updateById(location, update, "entity_" + STOCK_LOCATION);
//        }
//
//        if(j < resultList.size()) {
//            throw new BusinessException("拣料区无空闲空位");
//        }
//        // todo for test 模拟容器移动到拣货区 -----------------------------------

        return resultList;
    }

//    /**
//     * 根据多个原材料入库单id申请容器
//     */
//    @Transactional(rollbackFor = Exception.class)
//    public List<InStockWork> applyContainerByInOrderIds(List<String> ids) throws BusinessException {
//        if (CollectionUtils.isEmpty(ids)) {
//            return Collections.emptyList();
//        }
//        List<Document> inOrders = mongoTemplate.find(Query.query(newCriteria().and("_id").in(
//                ids.stream().map(ObjectId::new).collect(Collectors.toList())
//        )), Document.class, "entity_" + RAW_MATERIAL_IN_STOCK);
//        if (CollectionUtils.isEmpty(inOrders)) {
//            return Collections.emptyList();
//        }
//
//        List<InStockWork> inStockWorks = applyContainerByInOrders(inOrders.stream().map(x -> x.getString("inOrder"))
//                .collect(Collectors.toList()), A);
//        inStockWorks.addAll(applyContainerByInOrders(inOrders.stream().map(x -> x.getString("inOrder"))
//                .collect(Collectors.toList()), B));
//        return inStockWorks;
//    }

    public List<Document> doListToApplyParam(ToApplyParam param, String orderTable, String orderDtlTable) throws BusinessException {
        String inStockType = param.getInStockType();
        if (InStockType.productionInStock.getCode().equals(inStockType)) {
            return Collections.emptyList();
        }
        String materialLevel = param.getMaterialLevel();
        if (StringUtils.isBlank(materialLevel)) {
            return Collections.emptyList();
        }

        String abc = CommonConstant.getAbc(materialLevel);
        if (abc == null) {
            throw new BusinessException("未知的ABC分类");
        }

        Date startTime = Optional.ofNullable(param.getStartTime()).map(Date::new).orElse(null);
        Date endTime = Optional.ofNullable(param.getEndTime()).map(Date::new).orElse(null);
        List<AggregationOperation> operations = new LinkedList<>();
        Criteria criteria = newCriteria().and("isApplyContainer").is(false).and("abc").is(abc);
        if (startTime != null && endTime != null) {
            criteria = criteria.and("createTime").gte(startTime).lt(endTime);
        }
        operations.add(Aggregation.match(criteria));
        List<Document> dtls = aggregateResult(operations, "entity_" + orderDtlTable);
        Map<String, List<Document>> dtlMapList = new LinkedHashMap<>();
        dtls.forEach(x -> dtlMapList.computeIfAbsent(x.getString("inOrder"), m -> new ArrayList<>()).add(x));
        List<Document> inOrders = mongoTemplate.find(Query.query(newCriteria().and("inOrder").in(dtlMapList.keySet())), Document.class,
                "entity_" + orderTable);
        inOrders.forEach(x -> x.put("dtls", dtlMapList.get(x.getString("inOrder"))));
        return inOrders;
    }

    /**
     * 查看申请成功的入库单明细
     */
    public List<Document> doListInStoking(ToApplyParam param, List<String> inStockTypes) {
        String materialLevel = param.getMaterialLevel();
        if (StringUtils.isBlank(materialLevel) || CollectionUtils.isEmpty(inStockTypes)) {
            return Collections.emptyList();
        }
        String abc = getAbc(materialLevel);
        List<AggregationOperation> operations = new LinkedList<>();
        Criteria criteria = newCriteria().and("inStatus").is(newDocument("id", new ObjectId(applyOK.getCode())))
                .and("inStockType.id").in(inStockTypes.stream().map(ObjectId::new).collect(Collectors.toList()))
                .and("abc").is(abc);
        operations.add(Aggregation.match(criteria));
        return aggregateResult(operations, "entity_" + IN_STOCK_TASK);
    }

    public List<InStockWork> applyContainerByInOrdersV2(List<String> inOrders, String orderTable, String orderDtlTable,
                                                        String abc) throws BusinessException {
        if (CollectionUtils.isEmpty(inOrders)) {
            return Collections.emptyList();
        }

        // 查询单据头信息
        List<Document> documents = mongoTemplate.find(Query.query(newCriteria().and("inOrder").in(inOrders)), Document.class,
                "entity_" + orderTable);
        Map<String, Document> headMap = new HashMap();
        documents.forEach(x -> {
            headMap.put(x.getString("inOrder"), x);
        });

        // 根据当前存在的入库作业中的匹配数量，去计算当前需要申请容器的单据明细数量做扣减（使用计划数量 - 匹配数量）
        // 当扣减的结果是0，表示无需申请容器，删除这个明细
        // 当扣减结果大于0，表示还能继续申请容器。
        List<InStockWork> isws = mongoTemplate.find(
                Query.query(newCriteria().and("inOrder").in(inOrders)), InStockWork.class, "entity_" + IN_STOCK_TASK);
        Map<String, Double> matchedMap = new HashMap();
        isws.forEach(x -> {
            Double val = matchedMap.computeIfAbsent(x.getInOrderDtlId(), m -> 0D);
            matchedMap.put(x.getInOrderDtlId(), val + x.getMatchNum());
        });
        Query query = Query.query(newCriteria().and("inOrder").in(inOrders).and("isApplyContainer").is(false).and("abc").is(abc));
        query = query.with(Sort.by(Sort.Direction.ASC, "inOrder"));
        List<Document> dtls = mongoTemplate.find(query, Document.class, "entity_" + orderDtlTable);
        for (int i = dtls.size() - 1; i > -1; i--) {
            Document dtl = dtls.get(i);
            double planNum = dtl.getDouble("planInNum");
            double matched = matchedMap.getOrDefault(dtl.get("id").toString(), 0D);
            if (planNum <= matched) {
                dtls.remove(i);
            } else {
                dtl.put("planInNum", planNum - matched);
            }
        }
        if (CollectionUtils.isEmpty(dtls)) {
            log.info("没有可以申请容器的入库单明细");
            return Collections.emptyList();
        }

//        // 原来的逻辑备份
//        // 校验如果存在未完成的作业不能再次进行申请容器
//        Document checkTask = mongoTemplate.findOne(
//                Query.query(newCriteria().and("inOrder").in(inOrders)
//                        .and("inStatus.id").ne(new ObjectId(inStockOK.getCode()))),
//                Document.class, "entity_" + IN_STOCK_TASK);
//        if (checkTask != null) {
//            throw new BusinessException(String.format("入库单[%s]存在未执行完成的上架作业，不能重复申请容器", inOrders));
//        }
//        Query query = Query.query(newCriteria().and("inOrder").in(inOrders).and("isApplyContainer").is(false).and("abc").is(abc));
//        query = query.with(Sort.by(Sort.Direction.ASC, "inOrder"));
//        List<Document> dtls = mongoTemplate.find(query, Document.class, "entity_" + orderDtlTable);
//        if (CollectionUtils.isEmpty(dtls)) {
//            log.info("没有可以申请容器的入库单明细");
//            return Collections.emptyList();
//        }

        // 外挂容器与库位的关系
        Map<String, String> cnLocMap = new HashMap<>();

        // 容器绑定信息，key: 容器编码 / 容器编码:1 / 容器编码:2
        // 二层key：颜色:语种
        Map<String, Map<String, Document>> containerBindMap = new LinkedHashMap<>();
        Set<String> containerAppliedSet = new HashSet<>();

        // 对入库明细进行分组，来进行合并操作
        // 分组规则：容器类型 -》 物料编码:  批次列表
        Map<String, Map<String, List<Document>>> dtlMap = new HashMap<>();
        // 提取所有的物料编码
        Set<String> materialCodes = dtls.stream().map(x -> x.getString("materialCode")).collect(Collectors.toSet());
        // 查询对应的规则
        List<Document> rules = mongoTemplate.find(Query.query(newCriteria().and("materialCode").in(materialCodes)), Document.class,
                "entity_" + STOCK_RULE);
        // 整理数据
        Map<String, Document> ruleMap = rules.stream().collect(Collectors.toMap(x -> x.getString("materialCode"), x -> x, (o1, o2) -> o1));
        for (String materialCode : materialCodes) {
            if (!ruleMap.containsKey(materialCode)) {
                throw new BusinessException(String.format("物料[%s]的存储规则不存在，无法申请容器", materialCode));
            }
        }
        for (Document dtl : dtls) {
            String materialCode = dtl.getString("materialCode");
            Document rule = ruleMap.get(materialCode);
            String containerType = Optional.ofNullable(rule.get("containerType", Document.class)).map(c -> c.getString("id")).orElse(null);
            if (containerType == null) {
                throw new BusinessException(String.format("物料[%s]的存储规则未指定容器类型，无法申请容器", materialCode));
            }
            Map<String, List<Document>> m1 = dtlMap.computeIfAbsent(containerType, m -> new HashMap<>());
            m1.computeIfAbsent(materialCode, k -> new ArrayList<>()).add(dtl);
        }

        // 对每个容器进行单独的处理
        for (String containerType : dtlMap.keySet()) {
            Map<String, List<Document>> dtlByMaterial = dtlMap.get(containerType);
            // 分隔混放
            if (containerType.equals(partitionBox.getCode()) || containerType.equals(partitionBox3.getCode()) || containerType.equals(partitionBox4.getCode())
                    || containerType.equals(bigRack4.getCode()) || containerType.equals(lightweight6.getCode()) || containerType.equals(productBox2.getCode())) {
                // 带隔板的容器，允许混物料
                // 分成两种情况分开处理：可以混批次 与 不能混批次
                // 不能混批次，以批次分组
                Map<String, List<Document>> dtlByBatchNo = new LinkedHashMap<>();
                // 可以混批次，按照库区进行分组
                Map<String, List<Document>> mixedMaterialDtlByArea = new LinkedHashMap<>();
                for (String materialCode : dtlByMaterial.keySet()) {
                    Document rule = ruleMap.get(materialCode);
                    boolean isMixedBatch = ConvertUtils.convertIfPossible(rule.get("isMixedBatch"), Boolean.class, false);
                    if (isMixedBatch) {
                        List<String> areas = rule.getList("areas", String.class);
                        Collections.sort(areas);
                        String areaStr = areas.stream().collect(Collectors.joining(";"));
                        mixedMaterialDtlByArea.computeIfAbsent(areaStr, x -> new ArrayList<>()).addAll(dtlByMaterial.get(materialCode));
                    } else {
                        List<Document> mixBatchList = dtlByMaterial.get(materialCode);
                        mixBatchList.forEach(x -> {
                            String batchNo = x.getString("batchNo");
                            if(batchNo == null) {
                                batchNo = "";
                            }
                            dtlByBatchNo.computeIfAbsent(batchNo + ":" + x.getString("materialCode"), m -> new ArrayList<>()).add(x);
                        });
                    }
                }

                // 处理不能混批次的情况
                // 查询未满的容器，指定批次
                for (String key : dtlByBatchNo.keySet()) {
                    List<Document> batchDtls = dtlByBatchNo.get(key);
                    Collections.sort(batchDtls, Comparator.comparing(o -> o.getDouble("planInNum")));
                    String batchNo = key.split(":")[0];
                    String materialCode = key.split(":")[1];
                    Document rule = ruleMap.get(materialCode);
                    List<String> areas = rule.getList("areas", String.class);
                    double capacity = rule.getDouble("capacity");

                    // 查询满足条件的容器列表
                    List<Document> matchContainers = stockService.queryMatchContainers(containerType, areas,
                            materialCode, batchNo, capacity,
                            containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                    matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                    int matchIndex = 0, i = 0;
                    for (; i < batchDtls.size(); i++) {
                        Document inStockDtl = batchDtls.get(i);
                        log.info("分隔混放，处理不能混批次的情况。物料[{}]批次[{}]申请容器：", materialCode, batchNo);
                        // 匹配物料到未满容器中
                        matchIndex = matchMaterialInContainer(inStockDtl, matchIndex, matchContainers, capacity,
                                containerAppliedSet, containerType, materialCode, containerBindMap);
                        if (matchIndex >= matchContainers.size()) {
                            break;
                        }
                    }
                    if (i < batchDtls.size()) {
                        // 进入此逻辑代表容器不足，不足以装当前的入库物料
                        throw new BusinessException("申请失败，当前容器不满足数量要求");
                    }
                    containerAppliedSet.addAll(containerBindMap.keySet());
                }


                // 处理可以混批次的情况：混物料混批
                // 每个分区请求申请容器
                for (String key : mixedMaterialDtlByArea.keySet()) {
                    String[] areas = key.split(";");
                    Map<String, List<Document>> handleDtlByMaterial = new LinkedHashMap<>();
                    mixedMaterialDtlByArea.get(key).forEach(x -> handleDtlByMaterial.computeIfAbsent(x.getString("materialCode"), m -> new ArrayList<>()).add(x));
                    for (String materialCode : handleDtlByMaterial.keySet()) {
                        Document rule = ruleMap.get(materialCode);
                        double capacity = rule.getDouble("capacity");
                        List<Document> mixedBatchDtls = dtlByMaterial.get(materialCode);
                        Collections.sort(mixedBatchDtls, Comparator.comparing(o -> o.getDouble("planInNum")));

                        // 查询满足装的容器列表
                        List<Document> matchContainers = stockService.queryMatchContainers(containerType, Arrays.asList(areas),
                                materialCode, null, capacity,
                                containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                        matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                        int matchIndex = 0, i = 0;
                        for (; i < mixedBatchDtls.size(); i++) {
                            log.info("分隔混放，处理可以混批次的情况：混物料混批。物料[{}]申请容器：", materialCode);
                            Document inStockDtl = mixedBatchDtls.get(i);
                            // 匹配物料到未满容器中
                            matchIndex = matchMaterialInContainer(inStockDtl, matchIndex, matchContainers, capacity,
                                    containerAppliedSet, containerType, materialCode, containerBindMap);
                            if (matchIndex >= matchContainers.size()) {
                                break;
                            }
                        }
                        if (i < mixedBatchDtls.size()) {
                            // 进入此逻辑代表容器不足，不足以装当前的入库物料
                            throw new BusinessException("申请失败，当前容器不满足数量要求");
                        }
                        containerAppliedSet.addAll(containerBindMap.keySet());
                    }
                }

            } else if (containerType.equals(mixedMaterial.getCode())) {
                // 混物料存放，容器不分隔
                // 分两种情况
                // [A1, 001], [A2, 002], [A3, 003], [A1, 002] -- 混物料混批次: A1, A2, A3 相同的混物料组，可以混批次
                // [A5, 001], [A6, 001]                       --  混物料不混批次，A5, A6 相同的混物料组，都不能混批次
                Set<String> materialGroups = new HashSet<>();
                // 不能混批次的分组为 -> 批次：物料组：物料列表
                Map<String, Map<String, List<Document>>> mixedMaterialMap = new LinkedHashMap<>();
                // 可以混批次的分组为 -> 物料组：物料列表
                Map<String, List<Document>> mixedBatchNoMixedMaterialMap = new LinkedHashMap<>();
                for (String materialCode : dtlByMaterial.keySet()) {
                    // 查询物料混放规则
                    List<Document> mixedRules = simpleFind("entity_" + MATERIAL_MIX_RULE, "materialCode", materialCode);
                    assertBiz(() -> CollectionUtils.isEmpty(mixedRules), "物料[%s]未设置混放规则", materialCode);
                    assertBiz(() -> mixedRules.size() > 1, "物料[%s]的混放规则必须唯一，不能又多个", materialCode);
                    Document mixedRule = mixedRules.get(0);
                    String mixedGroup = mixedRule.getString("groupCode");
                    materialGroups.add(mixedGroup);
                    Document rule = ruleMap.get(materialCode);
                    boolean isMixedBatch = ConvertUtils.convertIfPossible(rule.get("isMixedBatch"), Boolean.class, false);
                    if (isMixedBatch) {
                        mixedBatchNoMixedMaterialMap.computeIfAbsent(mixedGroup, k -> new ArrayList<>()).addAll(dtlByMaterial.get(materialCode));
                    } else {
                        List<Document> materialDtls = dtlByMaterial.get(materialCode);
                        for (Document x : materialDtls) {
                            String batchNo;
                            if(x.getString("batchNo") == null) {
                                batchNo = "";
                            } else {
                                batchNo = x.getString("batchNo");
                            }
                            assertBiz(() -> StringUtils.isBlank(batchNo), "物料[%s]设置不能混批次存放，入库必须带有批次号", materialCode);
                            Map<String, List<Document>> map = mixedMaterialMap.computeIfAbsent(batchNo, k -> new HashMap<>());
                            map.computeIfAbsent(mixedGroup, k -> new ArrayList<>()).add(x);
                        }
                    }
                }

                // 混物料组下所有的物料
                List<MixedMaterialGroup> mixedMaterialGroupList = mongoTemplate.find(Query.query(newCriteria().and("groupCode").in(materialGroups)),
                        MixedMaterialGroup.class, "entity_" + MATERIAL_MIX_RULE);
                Map<String, List<String>> mixedGroupMaterialMap = new LinkedHashMap<>();
                mixedMaterialGroupList.forEach(x -> {
                    mixedGroupMaterialMap.computeIfAbsent(x.getGroupCode(), k -> new LinkedList<>()).add(x.getMaterialCode());
                });

                // 处理可以混批次的
                for (String group : mixedBatchNoMixedMaterialMap.keySet()) {
                    List<Document> inStockDtls = mixedBatchNoMixedMaterialMap.get(group);
                    List<String> groupMaterialCodes = mixedGroupMaterialMap.get(group);
                    List<String> requireMaterialCodes = inStockDtls.stream().map(x -> x.getString("materialCode")).collect(Collectors.toList());
                    Set<String> allMaterialCodes = new HashSet<>();
                    allMaterialCodes.addAll(groupMaterialCodes);
                    allMaterialCodes.addAll(requireMaterialCodes);
                    // 校验存储规则
                    List<String> areas = null;
                    Map<String, Double> capacityMapByMaterialCode = new HashMap<>();
                    for (String mc : allMaterialCodes) {
                        if(!ruleMap.containsKey(mc)) {
                            Document ruleTmp = mongoTemplate.findOne(Query.query(newCriteria().and("materialCode").is(mc)), Document.class,
                                    "entity_" + STOCK_RULE);
                            if(ruleTmp == null) {
                                throw new BusinessException(String.format("物料[%s]没有设置存储规则", mc));
                            }
                            ruleMap.put(mc, ruleTmp);
                        }
                        Document rule = ruleMap.get(mc);
                        if (areas == null) {
                            areas = rule.getList("areas", String.class);
                        } else {
                            List<String> areas2 = rule.getList("areas", String.class);
                            if (!(areas.containsAll(areas2) && areas2.containsAll(areas))) {
                                throw new BusinessException(String.format("物料[%s]与物料[%s]可以混放，但存储规则中指定的存储区域不一致",
                                        requireMaterialCodes.get(0), mc));
                            }
                        }
                        capacityMapByMaterialCode.put(mc, ConvertUtils.convertIfPossible(
                                rule.get("capacity"), Double.class, 0D));
                    }

                    List<Document> matchContainers = stockService.queryMatchMixedMaterialContainers(areas, groupMaterialCodes, null,
                            capacityMapByMaterialCode,
                            containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                    matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                    // 按照容器分组
                    Map<String, List<Document>> matchContainerMap = new LinkedHashMap<>();
                    matchContainers.forEach(x -> matchContainerMap.computeIfAbsent(x.getString("containerCode"), k -> new ArrayList<>()).add(x));
                    final String partitionSide = "1";
                    List<Document> consumerList = new ArrayList<>();
                    consumerList.addAll(inStockDtls);
                    Collections.reverse(consumerList);
                    for (String containerCode : matchContainerMap.keySet()) {
                        List<Document> matchContainerList = matchContainerMap.get(containerCode);
                        cm:
                        for (Document matchContainer : matchContainerList) {
                            for (int i = consumerList.size() - 1; i > -1; i--) {
                                Double use = matchContainer.getDouble("use");
                                String materialCode = matchContainer.getString("materialCode");
                                Double remainQty = capacityMapByMaterialCode.get(materialCode) - use;
                                if (remainQty <= 0) {
                                    // 容器耗尽
                                    continue cm;
                                }

                                Document consumer = consumerList.get(i);
                                String consumerMaterialCode = consumer.getString("materialCode");
                                if (!materialCode.equals(consumerMaterialCode)) {
                                    continue;
                                }
                                Double planNum = consumer.getDouble("planInNum");
                                if (remainQty >= planNum) {
                                    matchContainer.put("use", use + planNum);
                                    createContainerBind(containerCode, partitionSide, containerType, materialCode,
                                            consumer, planNum, matchContainer, containerBindMap);
                                    consumerList.remove(i);
                                } else {
                                    matchContainer.put("use", use + remainQty);
                                    createContainerBind(containerCode, partitionSide, containerType, materialCode,
                                            consumer, remainQty, matchContainer, containerBindMap);
                                    consumer.put("planInNum", planNum - remainQty);
                                }
                            }
                        }
                    }
                    if (consumerList.size() > 0) {
                        // 进入此逻辑代表容器不足，不足以装当前的入库物料
                        log.info("混物料混批次申请单：{}", mixedBatchNoMixedMaterialMap);
                        log.info("混物料混批次容器匹配情况：{}", matchContainers);
                        log.info("混物料混批次申请单未匹配：{}", consumerList);
                        throw new BusinessException("申请失败，当前容器不满足数量要求");
                    }
                    containerAppliedSet.addAll(containerBindMap.keySet());
                }

                // 处理不能混批次的
                for (String batchNo : mixedMaterialMap.keySet()) {
                    Map<String, List<Document>> sameBatchMixedGroupMaterialMap = mixedMaterialMap.get(batchNo);
                    for (String group : sameBatchMixedGroupMaterialMap.keySet()) {
                        List<Document> inStockDtls = mixedBatchNoMixedMaterialMap.get(group);
                        List<String> groupMaterialCodes = mixedGroupMaterialMap.get(group);
                        List<String> requireMaterialCodes = inStockDtls.stream().map(x -> x.getString("materialCode")).collect(Collectors.toList());
                        // 校验存储规则
                        List<String> areas = null;
                        Map<String, Double> capacityMapByMaterialCode = new HashMap<>();

                        Set<String> allMaterialCodes = new HashSet<>();
                        allMaterialCodes.addAll(groupMaterialCodes);
                        allMaterialCodes.addAll(requireMaterialCodes);

                        for (String mc : allMaterialCodes) {
                            if(!ruleMap.containsKey(mc)) {
                                Document ruleTmp = mongoTemplate.findOne(Query.query(newCriteria().and("materialCode").is(mc)), Document.class,
                                        "entity_" + STOCK_RULE);
                                if(ruleTmp == null) {
                                    throw new BusinessException(String.format("物料[%s]没有设置存储规则", mc));
                                }
                                ruleMap.put(mc, ruleTmp);
                            }
                            Document rule = ruleMap.get(mc);
                            if (areas == null) {
                                areas = rule.getList("areas", String.class);
                            } else {
                                List<String> areas2 = rule.getList("areas", String.class);
                                if (!(areas.containsAll(areas2) && areas2.containsAll(areas))) {
                                    throw new BusinessException(String.format("物料[%s]与物料[%s]可以混放，但存储规则中指定的存储区域不一致",
                                            requireMaterialCodes.get(0), mc));
                                }
                            }
                            capacityMapByMaterialCode.put(mc, ConvertUtils.convertIfPossible(
                                    rule.get("capacity"), Double.class, 0D));
                        }

                        List<Document> matchContainers = stockService.queryMatchMixedMaterialContainers(areas, groupMaterialCodes, batchNo,
                                capacityMapByMaterialCode,
                                containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                        matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                        // 按照容器分组
                        Map<String, List<Document>> matchContainerMap = new LinkedHashMap<>();
                        matchContainers.forEach(x -> matchContainerMap.computeIfAbsent(x.getString("containerCode"), k -> new ArrayList<>()).add(x));
                        final String partitionSide = "1";
                        List<Document> consumerList = new ArrayList<>();
                        consumerList.addAll(inStockDtls);
                        Collections.reverse(consumerList);
                        for (String containerCode : matchContainerMap.keySet()) {
                            List<Document> matchContainerList = matchContainerMap.get(containerCode);
                            cm:
                            for (Document matchContainer : matchContainerList) {
                                for (int i = consumerList.size() - 1; i > -1; i--) {
                                    Double use = matchContainer.getDouble("use");
                                    String materialCode = matchContainer.getString("materialCode");
                                    Double remainQty = capacityMapByMaterialCode.get(materialCode) - use;
                                    if (remainQty <= 0) {
                                        // 容器耗尽
                                        continue cm;
                                    }

                                    Document consumer = consumerList.get(i);
                                    String consumerMaterialCode = consumer.getString("materialCode");
                                    if (!materialCode.equals(consumerMaterialCode)) {
                                        continue;
                                    }
                                    Double planNum = consumer.getDouble("planInNum");
                                    if (remainQty >= planNum) {
                                        matchContainer.put("use", use + planNum);
                                        createContainerBind(containerCode, partitionSide, containerType, materialCode,
                                                consumer, planNum, matchContainer, containerBindMap);
                                        consumerList.remove(i);
                                    } else {
                                        matchContainer.put("use", use + remainQty);
                                        createContainerBind(containerCode, partitionSide, containerType, materialCode,
                                                consumer, remainQty, matchContainer, containerBindMap);
                                        consumer.put("planInNum", planNum - remainQty);
                                    }
                                }
                            }
                        }
                        if (consumerList.size() > 0) {
                            // 进入此逻辑代表容器不足，不足以装当前的入库物料
                            log.info("混物料同批次申请单：{}", mixedBatchNoMixedMaterialMap);
                            log.info("混物料同批次容器匹配情况：{}", matchContainers);
                            log.info("混物料同批次混物料申请单未匹配：{}", consumerList);
                            throw new BusinessException("申请失败，当前容器不满足数量要求");
                        }
                        containerAppliedSet.addAll(containerBindMap.keySet());
                    }
                }

            } else {
                // 不允许混物料，对每个物料单独处理
                for (String materialCode : dtlByMaterial.keySet()) {
                    Document rule = ruleMap.get(materialCode);
                    List<String> areas = rule.getList("areas", String.class);
                    boolean isMixedBatch = ConvertUtils.convertIfPossible(rule.get("isMixedBatch"), Boolean.class, false);
                    if (isMixedBatch) {
                        List<Document> mixedBatchDtls = dtlByMaterial.get(materialCode);
                        Collections.sort(mixedBatchDtls, Comparator.comparing(o -> o.getDouble("planInNum")));
                        double capacity = rule.getDouble("capacity");

                        // 可以混批次
                        // 匹配在库容器列表
                        List<Document> matchContainers = stockService.queryMatchContainers(containerType, areas, materialCode, null, capacity,
                                containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                        matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                        int matchIndex = 0, i = 0;
                        for (; i < mixedBatchDtls.size(); i++) {
                            log.info("不分隔板不混物料可以混批次情况。物料[{}]申请容器：", materialCode);
                            Document inStockDtl = mixedBatchDtls.get(i);
                            // 匹配物料到未满容器中
                            matchIndex = matchMaterialInContainer(inStockDtl, matchIndex, matchContainers, capacity,
                                    containerAppliedSet, containerType, materialCode, containerBindMap);
                            if (matchIndex >= matchContainers.size()) {
                                break;
                            }
                        }
                        if (i < mixedBatchDtls.size()) {
                            // 进入此逻辑代表容器不足，不足以装当前的入库物料
                            throw new BusinessException("申请失败，当前容器不满足数量要求");
                        }
                        containerAppliedSet.addAll(containerBindMap.keySet());

                    } else {
                        // 不能混批次
                        // 对当前的混批数据进行按照批次分组，依次来查询符合条件的容器
                        List<Document> mixedBatchDtls = dtlByMaterial.get(materialCode);
                        Map<String, List<Document>> batchNoDtlMap = new HashMap<>();
                        mixedBatchDtls.forEach(x -> {
                            String batchNo = x.getString("batchNo");
                            if(batchNo == null) {
                                batchNo = "";
                            }
                            batchNoDtlMap.computeIfAbsent(batchNo, k -> new ArrayList<>()).add(x);
                        });

                        for (String batchNo : batchNoDtlMap.keySet()) {
                            List<Document> batchDtls = batchNoDtlMap.get(batchNo);
                            Collections.sort(batchDtls, Comparator.comparing(o -> o.getDouble("planInNum")));
                            double capacity = rule.getDouble("capacity");

                            // 匹配在库容器列表
                            List<Document> matchContainers = stockService.queryMatchContainers(containerType, areas, materialCode, batchNo, capacity,
                                    containerAppliedSet.stream().map(x -> x.split(":")[0]).collect(Collectors.toList()));
                            matchContainers.forEach(x -> cnLocMap.put(x.getString("containerCode"), x.getString("locationCode")));
                            int matchIndex = 0, i = 0;
                            for (; i < mixedBatchDtls.size(); i++) {
                                log.info("不分隔板不混物料不混批次情况。物料[{}]批次[{}]申请容器：", materialCode, batchNo);
                                Document inStockDtl = mixedBatchDtls.get(i);
                                // 匹配物料到未满容器中
                                matchIndex = matchMaterialInContainer(inStockDtl, matchIndex, matchContainers, capacity,
                                        containerAppliedSet, containerType, materialCode, containerBindMap);
                                if (matchIndex >= matchContainers.size()) {
                                    break;
                                }
                            }
                            if (i < batchDtls.size()) {
                                // 进入此逻辑代表容器不足，不足以装当前的入库物料
                                throw new BusinessException("申请失败，当前容器不满足数量要求");
                            }

                            // 由于不能混批次，这个批次用过的容器都要记录下防止被重复使用
                            containerAppliedSet.addAll(containerBindMap.keySet());
                        }
                    }
                }
            }
        }

        if (containerBindMap.isEmpty()) {
            throw new BusinessException("申请失败，当前容器不满足数量要求");
        }

        // 根据容器绑定来创建任务
        List<Document> containBinds = new LinkedList<>();
        List<InStockWork> inStockWorks = new LinkedList<>();
        for (Map<String, Document> bindContainerMap : containerBindMap.values()) {
            for (Document bindContainer : bindContainerMap.values()) {
                containBinds.add(bindContainer);
                String containerCode = bindContainer.getString("containerCode");
                String materialCode = bindContainer.getString("materialCode");
                List<Document> inOrderObjs = bindContainer.getList("inOrders", Document.class);
                for (Document dtl : inOrderObjs) {
                    InStockWork task = new InStockWork();
                    task.setCode(dtl.getString("taskCode"));
                    task.setInOrder(dtl.getString("inOrder"));
                    task.setInOrderDtlId(dtl.get("id").toString());
                    task.setDtlSourceTable(orderDtlTable);
                    task.setInSource(dtl.getString("inSource"));
                    task.setInDetailSource(dtl.getString("inDetailSource"));
                    task.setInStatus(newDocument("id", new ObjectId(applyOK.getCode())));
                    task.setContainerType(bindContainer.get("containerType", Document.class));
                    task.setContainerCode(containerCode);
                    task.setPartitionSide(bindContainer.getString("partitionSide"));
                    task.setMaterialCode(materialCode);
                    task.setColorName(dtl.getString("colorName"));
                    task.setLanguageName(dtl.getString("languageName"));
                    task.setMaterialName(dtl.getString("materialName"));
                    String batchNo = dtl.getString("batchNo");
                    if(batchNo == null) {
                        batchNo = "";
                    }
                    task.setBatchNo(batchNo);
                    task.setPlanNum(bindContainer.getDouble("recommendedNum"));
                    task.setMatchNum(dtl.getDouble("matchNum"));
                    task.setAbc(abc);
                    String outLocationCode = cnLocMap.get(containerCode);
                    if(outLocationCode == null) {
                        throw new BusinessException(String.format("容器[%s]对应的库位数据丢失", containBinds));
                    }
                    task.setOutLocationCode(outLocationCode);

                    if (orderDtlTable.equals(RAW_MATERIAL_IN_STOCK_DETAIL)) {
                        // 采购入库
                        Document head = headMap.get(dtl.getString("inOrder"));
                        if (head != null && head.getEmbedded(Arrays.asList("sourceType", "id"), String.class).equals(SourceType.other.getCode())) {
                            task.setInStockType(newDocument("id", new ObjectId(InStockType.otherInStock.getCode())));
                        } else {
                            task.setInStockType(newDocument("id", new ObjectId(InStockType.rawMaterialInStock.getCode())));
                        }
                    } else {
                        // 销售退库
                        task.setInStockType(newDocument("id", new ObjectId(InStockType.salesReturnStock.getCode())));
                    }
                    task.setTenantId(tenantId);
                    inStockWorks.add(task);
                }
            }
        }
        Set<String> bindContainerCodes = inStockWorks.stream().map(InStockWork::getContainerCode).collect(Collectors.toSet());
        List<Document> updateContainers = mongoTemplate.find(Query.query(newCriteria().and("code").in(bindContainerCodes)), Document.class,
                "entity_" + CONTAINER);
        for (Document updateContainer : updateContainers) {
            Update update = new Update();
            update.set("lock", true);
            if (!versionUpdateById(updateContainer, update, "entity_" + CONTAINER)) {
                // 更新失败，需要重新申请
                throw new BusinessException("申请容器上锁失败，请重新申请");
            }
        }

        // 库位上锁
        Set<String> stockLocationCodes = inStockWorks.stream().map(InStockWork::getOutLocationCode).collect(Collectors.toSet());
        List<Document> updateStockLocations = mongoTemplate.find(Query.query(newCriteria().and("code").in(stockLocationCodes)),
                Document.class, "entity_" + STOCK_LOCATION);
        for (Document updateLocation : updateStockLocations) {
            Update update = new Update();
            update.set("lock", true);
            if (!versionUpdateById(updateLocation, update, "entity_" + STOCK_LOCATION)) {
                // 更新失败，需要重新申请
                throw new BusinessException("申请库位上锁失败，请重新申请");
            }
        }

        // 更新明细的申请容器状态
        Update dtlUpdate = new Update();
        dtlUpdate.set("isApplyContainer", true);
        for (Document dtl : dtls) {
            if (!versionUpdateById(dtl, dtlUpdate, "entity_" + orderDtlTable)) {
                // 更新失败，需要重新申请
                throw new BusinessException("申请失败");
            }
        }

        // 创建对应的 上架作业信息
        mongoTemplate.insert(inStockWorks, "entity_" + IN_STOCK_TASK);
        // 创建绑定容器数据
        mongoTemplate.insert(containBinds, "entity_" + BIND_CONTAINER);
        return inStockWorks;
    }

    private Document createContainerBind(String containerCode, String partitionSide, String containerType, String materialCode, Document inOrderDtl,
                                         double qty, Document containerDoc, Map<String, Map<String, Document>> containerBindMap) {
        String key = containerCode + ":" + partitionSide + ":" + materialCode;
        Map<String, Document> bindMap = containerBindMap.get(key);
        if (bindMap == null) {
            bindMap = new LinkedHashMap<>();
            containerBindMap.put(key, bindMap);
        }

        String key2 = inOrderDtl.get("colorName") + ":" + inOrderDtl.get("languageName");
        Document bind = bindMap.get(key2);
        if (bind == null) {
            bind = new Document();
            bindMap.put(key2, bind);
        }
        bind.put("code", UUID.randomUUID().toString().replace("-", ""));
        bind.put("partitionSide", partitionSide);
        bind.put("containerCode", containerCode);
        bind.put("containerType", newDocument("id", new ObjectId(containerType)));
        bind.put("optType", newDocument("id", new ObjectId(OptType.toBind.getCode())));
        bind.put("status", newDocument("id", new ObjectId(DataStatus.effect.getCode())));
        bind.put("materialCode", materialCode);
        bind.put("createTime", new Date());
        bind.put("tenantId", tenantId);
        bind.put("isDelete", 0);
        bind.put("warehouseCode", containerDoc.get("warehouseCode"));
        bind.put("areaCode", containerDoc.get("areaCode"));
        bind.put("locationCode", containerDoc.get("locationCode"));
        bind.put("colorName", inOrderDtl.get("colorName"));
        bind.put("languageName", inOrderDtl.get("languageName"));
//        containerBindMap.put(containerCode + ":" + partitionSide, bind);

        List<Document> inOrders = bind.getList("inOrders", Document.class);
        if (inOrders == null) {
            inOrders = new ArrayList<>();
        }

        Document inOrderDtlData = new Document(inOrderDtl);
        inOrderDtlData.put("matchNum", qty);
        inOrderDtlData.put("taskCode", new ObjectId().toString());
        inOrders.add(inOrderDtlData);
        bind.put("inOrders", inOrders);

        String batchNo = inOrderDtlData.getString("batchNo");
        bind.put("batchNo", batchNo);
        bind.put("recommendedNum", ConvertUtils.convertIfPossible(bind.get("recommendedNum"), Integer.class, 0) + qty);

        return bind;
    }

    /**
     * 匹配物料到容器，匹配单个物料到容器
     */
    private int matchMaterialInContainer(Document inStockDtl, int index, List<Document> matchContainers, double capacity,
                                         Set<String> containerAppliedSet, String containerType, String materialCode,
                                         Map<String, Map<String, Document>> containerBindMap) {
        Double planInRemainNum = inStockDtl.getDouble("planInRemainNum");
        if (planInRemainNum == null) {
            planInRemainNum = inStockDtl.getDouble("planInNum");
            inStockDtl.put("planInRemainNum", planInRemainNum);
        }

        while (index < matchContainers.size()) {
            Document container = matchContainers.get(index);
            Double remainQty = container.getDouble("remainQty");
            if (remainQty == null) {
                double stockQty = container.getDouble("use");
                remainQty = capacity - stockQty;
            }

            String containerCode = container.getString("containerCode");
            String partitionSide = ConvertUtils.convertIfPossible(container.get("partitionSide"), String.class, "1");
            String key = containerCode + ":" + partitionSide;
            if (containerAppliedSet.contains(key)) {
                log.info("容器[{}]隔板[{}]已在本次申请中消耗", containerCode, partitionSide);
                index += 1;
                continue;
            }
            if (remainQty >= planInRemainNum) {
                createContainerBind(containerCode, partitionSide, containerType, materialCode, inStockDtl, planInRemainNum, container, containerBindMap);
                container.put("realNum", remainQty - planInRemainNum);
                remainQty -= planInRemainNum;
                double matchNUm = planInRemainNum;
                planInRemainNum = 0D;
                log.info("容器[{}]隔板[{}]符合物料[{}]申请要求，匹配装入[{}]，需求还剩下[{}],容器还留存空间[{}]",
                        containerCode, partitionSide, materialCode, matchNUm, planInRemainNum, remainQty);
            } else {
                planInRemainNum -= remainQty;
                inStockDtl.put("planInRemainNum", planInRemainNum);
                containerAppliedSet.add(key);
                createContainerBind(containerCode, partitionSide, containerType, materialCode, inStockDtl, remainQty, container, containerBindMap);
                double matchNUm = planInRemainNum;
                remainQty = 0D;
                log.info("容器[{}]隔板[{}]符合物料[{}]申请要求，匹配装入[{}]，需求还剩下[{}],容器还留存空间[{}]",
                        containerCode, partitionSide, materialCode, matchNUm, planInRemainNum, remainQty);
            }
            container.put("remainQty", remainQty);
            if (planInRemainNum <= 0) {
                break;
            }
            index += 1;
        }
        return index;
    }

    @Transactional(rollbackFor = Exception.class)
    public boolean doContainerBindMaterialByBindCode(String bindCode, int realNum) throws BusinessException {
        if (realNum <= 0) {
            return false;
        }

        Document bind = simpleFindOne("entity_" + BIND_CONTAINER, "code", bindCode);
        if (bind == null) {
            throw new BusinessException("容器绑定数据不存在");
        }
        String optType = DocumentUtils.unwrapDictItem(bind.get("optType", Document.class));
        if (!OptType.toBind.getCode().equals(optType)) {
            throw new BusinessException("必须是【未绑定】状态下才能进行绑定");
        }

        List<Document> inOrders = bind.getList("inOrders", Document.class);
        List<String> taskCodes = inOrders.stream().map(x -> x.getString("taskCode")).collect(Collectors.toList());
        List<InStockWork> inStockTasks = mongoTemplate.find(Query.query(
                        newCriteria().and("code").in(taskCodes).and("inStatus").is(newDocument("id", new ObjectId(applyOK.getCode())))),
                InStockWork.class, "entity_" + IN_STOCK_TASK);
        if (CollectionUtils.isEmpty(inStockTasks)) {
            throw new BusinessException("数据非法，上架作业不存在");
        }

        // 根据入库单进行分组，匹配的数量之和就是最大数量
        double maxNum = 0D;
        for (InStockWork inStockTask : inStockTasks) {
            maxNum += inStockTask.getMatchNum();
        }
        if (realNum > maxNum) {
            throw new BusinessException("实际数量不能大于推荐数量");
        }

        Update update = new Update();
        update.set("realNum", realNum);
        update.set("isBind", true);
        update.set("optType", newDocument("id", new ObjectId(OptType.bind.getCode())));
        if (!updateById(bind, update, "entity_" + BIND_CONTAINER)) {
            return false;
        }

        // 依次更新实际数量
        double realNumTemp = realNum;
        for (InStockWork inStockTask : inStockTasks) {
            Update update2 = new Update();
            update2.set("inStatus", newDocument("id", new ObjectId(InStatus.containerBind.getCode())));
            double match = inStockTask.getMatchNum();
            if (realNumTemp >= match) {
                update2.set("realNum", match);
                inStockTask.setRealNum(match);
                realNumTemp -= match;
            } else {
                update2.set("realNum", realNumTemp);
                inStockTask.setRealNum(realNumTemp);
                realNumTemp = 0D;
            }
            updateById(inStockTask.get_id().toString(), update2, "entity_" + IN_STOCK_TASK);
        }

        return true;
    }

    /**
     * 绑定物料到容器，申请库位，启动入库
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean doStartStock(ContainerBindMaterialParam param) throws BusinessException {
        // 容器绑定物料
        if (!doContainerBindMaterialByBindCode(param.getBindCode(), param.getRealNum())) {
            throw new BusinessException("容器绑定物料失败");
        }

        List<InStockWork> bindTasks = mongoTemplate.find(Query.query(newCriteria().and("containerCode").is(param.getContainerCode())),
                InStockWork.class, "entity_" + IN_STOCK_TASK);
        String abc = bindTasks.get(0).getAbc();
        long count = bindTasks.stream().filter(x -> x.getInStatus().getString("id").equals(applyOK.getCode())).count();
        if (count <= 0) {
            // 申请库位
            Document endLocation = doApplyLocationByContainerCode(param.getContainerCode());
            if (endLocation == null) {
                throw new BusinessException("申请库位失败");
            }

            List<String> codes = bindTasks.stream().map(InStockWork::getCode).collect(Collectors.toList());

            String startLocationCode;
            if (abc.equals(A)) {
                Document startLocation = simpleFindOne("entity_" + STOCK_LOCATION,
                        "containerCode", param.getContainerCode());
                startLocationCode = startLocation.getString("code");
            } else if (abc.equals(B)) {
                // B类物料任务上架任务需要固定指定起点库位为   B类原材料拣货出口库区的第一个库位
                String startLocationArea = AreaCodeDict.bMatPickingExit.getText();
                Document locationDoc = simpleFindOne("entity_" + STOCK_LOCATION, "area", startLocationArea);
                if (locationDoc == null) {
                    throw new BusinessException(String.format("库区[%s]下无库位", startLocationArea));
                }
                startLocationCode = locationDoc.getString("code");
            } else {
                throw new BusinessException("未知的物料ABC分类无法执行");
            }

            Task task = Task.builder()
                    .taskCode(UUID.randomUUID().toString().replaceAll("-", ""))
                    .taskType(TaskType.STOCK_IN.getId())
                    .startLoc(startLocationCode)
                    .endArea(endLocation.getString("area"))
                    .endLoc(endLocation.getString("code"))
                    .podCode(param.getContainerCode())
                    .business(IN_STOCK_BIZ)
                    .operationNo(codes)
                    .build();
            wmsTaskService.saveTaskInfo(task);

//            // todo mock 更新库存 start----------------------------------------------
//            Document inStockTask = simpleFindOne("entity_" + IN_STOCK_TASK,
//                    "containerCode", param.getContainerCode(), "inStatus", DocumentUtils.wrapDictItem(InStatus.inStocking.getCode()));
//
//            // 校验需要存在入库作业
//            if (inStockTask == null) {
//                throw new BusinessException("mock 更新库存: 上架作业不存在");
//            }
//            stockService.updateInStock(inStockTask.getString("code"));
//
//            // 模拟容器移到目标库位
//            Document pickLocation = simpleFindOne("entity_" + STOCK_LOCATION,
//                    "containerCode", param.getContainerCode());
//            Update update = new Update();
//            update.unset("containerCode");
//            updateById(pickLocation, update, "entity_" + STOCK_LOCATION);
//
//            Update update2 = new Update();
//            update2.set("containerCode", param.getContainerCode());
//            update2.set("lock", false);
//            updateById(endLocation, update2, "entity_" + STOCK_LOCATION);
//            // todo mock 更新库存 end  ----------------------------------------------

            return true;
        }

//        // 如果上架作业状态变更为【已绑定】，则开始申请库位执行入库。如果不是，表示这个容器还能装其他物料并等待装完
//        Document bindTask = simpleFindOne("entity_" + IN_STOCK_TASK,
//                "containerCode", param.getContainerCode(), "inStatus", DocumentUtils.wrapDictItem(InStatus.containerBind.getCode()));
//        if (bindTask != null) {
//            // 申请库位
//            Document endLocation = applyLocationByContainerCode(param.getContainerCode());
//            if (endLocation == null) {
//                throw new BusinessException("申请库位失败");
//            }
//
//            // wcs
//            // 创建机器人上架任务
//            Document thisInStockTask = simpleFindOne("entity_" + IN_STOCK_TASK,
//                    "containerCode", param.getContainerCode(), "inStatus", DocumentUtils.wrapDictItem(InStatus.inStocking.getCode()));
//            if (thisInStockTask == null) {
//                throw new BusinessException("上架作业不存在");
//            }
//            Document startLocation = simpleFindOne("entity_" + STOCK_LOCATION,
//                    "containerCode", param.getContainerCode());
//            String startLocationCode = startLocation.getString("code");
//            Task task = Task.builder()
//                    .taskCode(UUID.randomUUID().toString().replaceAll("-", ""))
//                    .taskType(TaskType.STOCK_IN.getId())
//                    .startLoc(startLocationCode)
//                    .endArea(endLocation.getString("area"))
//                    .endLoc(endLocation.getString("code"))
//                    .podCode(param.getContainerCode())
//                    .operationNo(Arrays.asList(thisInStockTask.getString("code")))
//                    .build();
//            taskService.saveTaskInfo(task);
//
////            // todo mock 更新库存 start----------------------------------------------
////            Document inStockTask = simpleFindOne("entity_" + IN_STOCK_TASK,
////                    "containerCode", param.getContainerCode(), "inStatus", DocumentUtils.wrapDictItem(InStatus.inStocking.getCode()));
////
////            // 校验需要存在入库作业
////            if (inStockTask == null) {
////                throw new BusinessException("mock 更新库存: 上架作业不存在");
////            }
////            stockService.updateInStock(inStockTask.getString("code"));
////
////            // 模拟容器移到目标库位
////            Document pickLocation = simpleFindOne("entity_" + STOCK_LOCATION,
////                    "containerCode", param.getContainerCode());
////            Update update = new Update();
////            update.unset("containerCode");
////            updateById(pickLocation, update, "entity_" + STOCK_LOCATION);
////
////            Update update2 = new Update();
////            update2.set("containerCode", param.getContainerCode());
////            update2.set("lock", false);
////            updateById(endLocation, update2, "entity_" + STOCK_LOCATION);
////            // todo mock 更新库存 end  ----------------------------------------------
//
//            return true;
//        }

        return true;
    }

    /**
     * 查询容器带绑定的信息
     */
    public Object doFindContainerToBind(String containerCode) throws BusinessException {
        // 查询绑定容器
        Criteria match = newCriteria().and("containerCode").is(containerCode)
                .and("optType").is(newDocument("id", new ObjectId(OptType.toBind.getCode())));
        List<AggregationOperation> operations = new LinkedList<>();
        operations.add(Aggregation.match(match));
        operations.add(Aggregation.lookup("entity_" + MATERIAL, "materialCode", "code", "material"));
        operations.add(Aggregation.unwind("material"));
        return aggregateFirstResult(operations, "entity_" + BIND_CONTAINER);
    }

    /**
     * 查询容器带绑定的信息，通过库位
     */
    public Object doFindContainerToBindByLocation(String locationCode) throws BusinessException {
        if (StringUtils.isBlank(locationCode)) {
            throw new BusinessException("库位编码必传");
        }
        Document document = simpleFindOne("entity_" + STOCK_LOCATION, "code", locationCode);
        if (document == null) {
            throw new BusinessException(String.format("库位[%s]不存在", locationCode));
        }
        String containerCode = document.getString("containerCode");
        if (StringUtils.isBlank(containerCode)) {
            throw new BusinessException(String.format("库位[%s]上无容器", locationCode));
        }
        return doFindContainerToBind(containerCode);
    }

    /**
     * 查询容器带绑定的信息
     */
    public Object doFindContainerToBindById(String bindId) throws BusinessException {
        // 查询绑定容器
        Criteria match = newCriteria().and("_id").is(new ObjectId(bindId))
                .and("optType").is(newDocument("id", new ObjectId(OptType.toBind.getCode())));
        List<AggregationOperation> operations = new LinkedList<>();
        operations.add(Aggregation.match(match));
        operations.add(Aggregation.lookup("entity_" + MATERIAL, "materialCode", "code", "material"));
        operations.add(Aggregation.unwind("material"));
        Document bind = aggregateFirstResult(operations, "entity_" + BIND_CONTAINER);
        if (bind == null) {
            throw new BusinessException("绑定容器数据不存在");
        }
        return bind;
    }

    public boolean doReportErp(String dtlId, String orderTable, String orderDtlTable) throws BusinessException {
        if (StringUtils.isBlank(dtlId)) {
            throw new BusinessException("id不能为空");
        }
        Document dtlData = simpleFindOne("entity_" + orderDtlTable, "_id", dtlId);
        if (dtlData == null) {
            throw new BusinessException(String.format("单据明细[%s]不存在", dtlId));
        }

        String erpBillId = dtlData.getString("erpBillId");
        if (StringUtils.isNoneBlank(erpBillId)) {
            throw new BusinessException("已经完成Erp上报，不能重复上报");
        }
        // 是否完成的入库，查询这个单据关联的所有的上架作业
        List<InStockWork> works = mongoTemplate.find(Query.query(newCriteria().and("inOrderDtlId").is(dtlId)),
                InStockWork.class, "entity_" + IN_STOCK_TASK);
        if (CollectionUtils.isEmpty(works)) {
            throw new BusinessException("未生成上架作业不能上报");
        }
        for (InStockWork work : works) {
            if (!work.getInStatus().getString("id").equals(inStockOK.getCode())) {
                throw new BusinessException("入库关联的作业未完成入库不能上报");
            }
        }

        // 查询单据头
        Document head = simpleFindOne("entity_" + orderTable, "inOrder", dtlData.getString("inOrder"));
        if (head == null) {
            throw new BusinessException(String.format("入库单头[%s]不存在", dtlData.getString("inOrder")));
        }

        String soapMethod;
        String param;
        String returnQName;
        ErpReturn erpReturn;

        // 采购入库、其他入库
        if (orderTable.equals(RAW_MATERIAL_IN_STOCK)) {
            Document qmsRawInStock = dtlData.get("qmsRawInStock", Document.class);
            if (qmsRawInStock == null) {
                // 其他入库
                ErpOtherInStock erpOtherInStock = new ErpOtherInStock();

                erpOtherInStock.setBizType(head.getString("erpBizType"));
                erpOtherInStock.setTransactionType(head.getString("erpTransactionType"));
                erpOtherInStock.setCostCenterOrgUnit(head.getString("erpCostCenterOrgUnit"));
                erpOtherInStock.setStorageOrgUnit(head.getString("erpStorageOrgUnit"));
                erpOtherInStock.setAdminOrgUnit(head.getString("erpAdminOrgUnit"));

                ErpOtherInStockEntry erpOtherInStockEntry = new ErpOtherInStockEntry();
                erpOtherInStockEntry.setMaterial(dtlData.getString("materialCode"));
                erpOtherInStockEntry.setQty(dtlData.getDouble("realInNum") + "");
                erpOtherInStockEntry.setWarehouse(dtlData.getString("erpWareHouse"));
                erpOtherInStockEntry.setCostObject(dtlData.getString("materialCode"));
                erpOtherInStockEntry.setCostItem(dtlData.getString("erpCostItem"));
                erpOtherInStock.setEntrys(Arrays.asList(erpOtherInStockEntry));

                ObjectMapper mapper = new ObjectMapper();
                mapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE);
                try {
                    param = mapper.writeValueAsString(erpOtherInStock);
                } catch (JsonProcessingException e) {
                    throw new BusinessException(e);
                }
                soapMethod = "CreateOtherInStock";
                returnQName = "CreateOtherInStockReturn";
            } else {
                // 采购入库
                ErpRawInStock erpRawInStock = new ErpRawInStock();
                erpRawInStock.setRuleCode(qmsRawInStock.getString("ruleNumber"));
                erpRawInStock.setSourceBillId(qmsRawInStock.getString("sourceBillId"));

                erpRawInStock.setBizType(head.getString("erpBizType"));
                erpRawInStock.setTransactionType(head.getString("erpTransactionType"));
                erpRawInStock.setCostCenterOrgUnit(head.getString("erpCostCenterOrgUnit"));
                erpRawInStock.setStorageOrgUnit(head.getString("erpStorageOrgUnit"));
                erpRawInStock.setAdminOrgUnit(head.getString("erpAdminOrgUnit"));

                ErpRawInStockEntry entry = new ErpRawInStockEntry();
                entry.setSourceEntryId(qmsRawInStock.getString("sourceBillEntryId"));
                entry.setMaterial(dtlData.getString("materialCode"));
                entry.setQty(dtlData.getDouble("realInNum") + "");
                entry.setWarehouse(dtlData.getString("erpWareHouse"));
                erpRawInStock.setEntrys(Arrays.asList(entry));

                ObjectMapper mapper = new ObjectMapper();
                mapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE);

                try {
                    param = mapper.writeValueAsString(erpRawInStock);
                } catch (JsonProcessingException e) {
                    throw new BusinessException(e);
                }
                soapMethod = "CreatePurInStock";
                returnQName = "CreatePurInStockReturn";
            }
        } else if (orderTable.equals(SALES_RETURN_STOCK)) {
            Document originData = dtlData.get("originData", Document.class);
            // 销售退库
            ErpSalesReturnStock erpSalesReturnStock = new ErpSalesReturnStock();
            erpSalesReturnStock.setRuleCode(originData.getString("ruleNumber"));
            erpSalesReturnStock.setSourceBillId(originData.getString("sourceBillId"));

            erpSalesReturnStock.setBizType(head.getString("erpBizType"));
            erpSalesReturnStock.setTransactionType(head.getString("erpTransactionType"));
            erpSalesReturnStock.setCostCenterOrgUnit(head.getString("erpCostCenterOrgUnit"));
            erpSalesReturnStock.setStorageOrgUnit(head.getString("erpStorageOrgUnit"));
            erpSalesReturnStock.setAdminOrgUnit(head.getString("erpAdminOrgUnit"));

            ErpSalesReturnStockEntry entry = new ErpSalesReturnStockEntry();
            entry.setSourceEntryId(originData.getString("sourceBillEntryId"));
            entry.setMaterial(dtlData.getString("materialCode"));
            entry.setQty(dtlData.getDouble("realInNum") + "");
            entry.setWarehouse(dtlData.getString("erpWareHouse"));
            erpSalesReturnStock.setEntrys(Arrays.asList(entry));

            ObjectMapper mapper = new ObjectMapper();
            mapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE);

            try {
                param = mapper.writeValueAsString(erpSalesReturnStock);
            } catch (JsonProcessingException e) {
                throw new BusinessException(e);
            }
            soapMethod = "CreateSaleReturnStock";
            returnQName = "CreateSaleReturnStockReturn";
        } else {
            throw new BusinessException("不支持的单据类型");
        }


        // 上报
        String result = erpReportService.erpReport(soapMethod, param, returnQName);
        ObjectMapper mapper2 = new ObjectMapper();
        try {
            Map map = mapper2.readValue(new ByteArrayInputStream(result.getBytes()), Map.class);
            erpReturn = mapper2.convertValue(map, ErpReturn.class);
        } catch (IOException e) {
            throw new BusinessException(e);
        }

        // 更新单据
        Update update = new Update();
        int status = erpReturn.getStatus();
        if (status == 0) {
            update.set("erpReport", true);
        } else {
            update.set("erpReport", false);
        }
        update.set("erpAudit", erpReturn.getStatus() == 0);
        update.set("erpMsg", erpReturn.getMsg());
        update.set("erpReportTime", new Date());
        update.set("erpBizDate", ConvertUtils.convertIfPossible(erpReturn.getBillDate(), Date.class));
        update.set("erpBillNumber", erpReturn.getBillNumber());
        update.set("erpBillId", erpReturn.getBillId());
        updateById(dtlData, update, "entity_" + orderDtlTable);

        return false;
    }

}
